package org.trinity.netdisk.service.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.trinity.netdisk.bo.CheckUserFileDo;
import org.trinity.netdisk.entity.File;
import org.trinity.netdisk.entity.FileType;
import org.trinity.netdisk.entity.User;
import org.trinity.netdisk.entity.UserFile;
import org.trinity.netdisk.enums.ResultCode;
import org.trinity.netdisk.exception.file.*;
import org.trinity.netdisk.exception.user.InvalidParamterException;
import org.trinity.netdisk.mapper.FileMapper;
import org.trinity.netdisk.mapper.TypeMapper;
import org.trinity.netdisk.mapper.UserFileMapper;
import org.trinity.netdisk.service.IFileService;
import org.trinity.netdisk.util.FileUtil;
import org.trinity.netdisk.util.JWTUtil;
import org.trinity.netdisk.util.ResponseWrapper;
import org.trinity.netdisk.vo.allFile.FileVO;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.TimeUnit;

@SuppressWarnings({"unused", "unchecked"})
@Service
@Transactional
public class FileServiceImpl implements IFileService {
    @Value("${chunkSize}")
    private Double chunkSize;

    @Value("${storage}")
    private String baseFilePath;

    private final Integer initIndex = 1;
    Logger logger = LoggerFactory.getLogger(FileServiceImpl.class);

    @Resource
    FileMapper fileMapper;

    @Resource
    TypeMapper typeMapper;

    @Resource
    UserFileMapper userFileMapper;

    @SuppressWarnings("rawtypes")
    @Autowired
    RedisTemplate redisTemplate;

    private final String redisCurrIndexKey = "currIndex";

    private final String redisFailedChunkKey = "failedChunk";

    private final int dirId = 5;

    @Override
    public List<FileVO> getFileByUserIdAndDirId(Long userId, Long dirId) {
        Queue<FileVO> sortFileVoQueue = new LinkedList<>();
        FileVO fileVO;
        List<FileVO> fileVOList = new LinkedList<>();
        List<UserFile> fileList = fileMapper.selectByUserIdAndDirId(userId, dirId);
        for (UserFile userFile : fileList) {
            fileVO = new FileVO();
            fileVO.setUserFile(userFile);
            //文件夹优先
            if (fileVO.getTypeId() == this.dirId) {
                fileVOList.add(fileVO);
            } else {
                sortFileVoQueue.add(fileVO);
            }
        }
        while (sortFileVoQueue.size() > 0) {
            fileVOList.add(sortFileVoQueue.poll());
        }
        return fileVOList;
    }

    @Override
    public List<FileType> selectAllFileType() {
        return fileMapper.selectAllFileType();
    }

    @Override
    public Long selectUserFileUsage(Long userId) {
        Long bytesUsage = fileMapper.selectUserFileUsage(userId);
        return bytesUsage == null ? 0 : bytesUsage;
    }

    @Override
    public UserFile selectUserFileByIdAndUserId(Long userId, Long userFileId) {
        return fileMapper.selectUserFileByIdAndUserId(userId, userFileId);
    }

    @Override
    public void createDir(UserFile userFile) {
        if(userFile.getName() == null || "".equals(userFile.getName())){
            throw new NoNameException();
        }
        UserFile target = new UserFile();
        target.setName(userFile.getName());
        target.setParentId(userFile.getParentId());
        target.setUserId(userFile.getUserId());
        if (fileMapper.selectByNameUserIdAndParentId(target).size() > 0) {
            throw new DuplicateNameException();
        }
        fileMapper.createDir(target);
    }

    @Override
    public void deleteFile(UserFile userFile) {
        UserFile deleteTarget;
        Queue<UserFile> userFiles = fileMapper.selectByUserIdAndDirId(userFile.getUserId(), userFile.getId());
        while (userFiles.size() > 0) {
            deleteTarget = userFiles.poll();
            userFiles.addAll(fileMapper.selectByUserIdAndDirId(deleteTarget.getUserId(), deleteTarget.getId()));
            if (fileMapper.deleteFile(deleteTarget) <= 0) {
                throw new DeleteFailedException();
            }
            redisTemplate.opsForValue().set(String.valueOf(deleteTarget.getId()), "#", 15, TimeUnit.DAYS);
        }
        if (fileMapper.deleteFile(userFile) <= 0) {
            throw new DeleteFailedException();
        }
        redisTemplate.opsForValue().set(String.valueOf(userFile.getId()), "#");
    }

    @Override
    public void deleteFileFromRecycleBin(UserFile userFile) {
        UserFile deleteTarget;
        Queue<UserFile> userFiles = fileMapper.selectFromRecycleBinByUserIdAndDirId(userFile.getUserId(), userFile.getId());
        while (userFiles.size() > 0) {
            deleteTarget = userFiles.poll();
            userFiles.addAll(fileMapper.selectFromRecycleBinByUserIdAndDirId(deleteTarget.getUserId(), deleteTarget.getId()));
            if (fileMapper.deleteFileFromRecycleBin(deleteTarget) <= 0) {
                throw new DeleteFailedException();
            }
            redisTemplate.delete(String.valueOf(deleteTarget.getId()));
        }
        if (fileMapper.deleteFileFromRecycleBin(userFile) <= 0) {
            throw new DeleteFailedException();
        }
        redisTemplate.delete(String.valueOf(userFile.getId()));
    }

    @Override
    public void recoverFromRecycleBin(UserFile userFile) {
        UserFile recoverTarget;
        Queue<UserFile> userFiles = fileMapper.selectFromRecycleBinByUserIdAndDirId(userFile.getUserId(), userFile.getParentId());
        while (userFiles.size() > 0) {
            recoverTarget = userFiles.poll();
            userFiles.addAll(fileMapper.selectFromRecycleBinByUserIdAndDirId(recoverTarget.getUserId(), recoverTarget.getParentId()));
            if (fileMapper.recoverFromRecycleBin(recoverTarget) <= 0) {
                throw new OperationFailedException();
            }
            redisTemplate.delete(String.valueOf(recoverTarget.getId()));
        }
        if (fileMapper.recoverFromRecycleBin(userFile) <= 0) {
            throw new OperationFailedException();
        }
        redisTemplate.delete(String.valueOf(userFile.getId()));
    }

    @Override
    public void deleteFileFromRecycleBinByUserFileId(Long userFileId) {
        if (fileMapper.deleteFileFromRecycleBinByUserFileId(userFileId) <= 0) {
            throw new DeleteFailedException();
        }
    }

    @Override
    public List<FileVO> getFileFromRecycleBinByUserIdAndDirId(Long userId, Long dirId) {
        FileVO fileVO;
        List<FileVO> fileVOList = new LinkedList<>();
        List<UserFile> fileList = fileMapper.selectFromRecycleBinByUserIdAndDirId(userId, dirId);
        for (UserFile userFile : fileList) {
            fileVO = new FileVO();
            fileVO.setUserFile(userFile);
            fileVOList.add(fileVO);
        }
        return fileVOList;
    }

    @Override
    public Integer chunkHandler(String md5, Long size) {
        HashOperations<String, String, Integer> hashOperations = redisTemplate.opsForHash();

        int currIndex = Optional.ofNullable(hashOperations.get(redisCurrIndexKey, md5)).orElse(0);
        //传到最后一块
        if (Math.ceil(size / chunkSize) == currIndex) {
            HashOperations<String, String, LinkedList<Integer>> listHashOperations = redisTemplate.opsForHash();
            Queue<Integer> failedChunkList = listHashOperations.get("failedChunk", md5);
            //没失败块
            if (failedChunkList == null || failedChunkList.size() == 0) {
                HashOperations<String, String, LinkedList<Integer>> failedHashOperation = redisTemplate.opsForHash();
                hashOperations.delete(redisCurrIndexKey, md5);
                failedHashOperation.delete(redisFailedChunkKey, md5);
                return 0;
            }
            //返回第一块失败块
            else {
                return failedChunkList.poll();
            }
        }
        //返回下一块
        return currIndex + 1;
    }

    /**
     * 下载用户文件
     *
     * @param userFileId 用户文件id
     * @return CheckUserFileDo
     */
    @Override
    public CheckUserFileDo getFilePath(Long userFileId, Long userId) {
        UserFile userFileById = userFileMapper.findUserFileByIdAndUserId(userFileId, userId);
        CheckUserFileDo checkUserFileDo = new CheckUserFileDo();
        if (userFileById == null) {
            checkUserFileDo.setCheckStatus(false);
        } else {
            checkUserFileDo.setCheckStatus(true);
            File file = fileMapper.selectFileByFileId(userFileById.getFileId());
            checkUserFileDo.setFilePath(file.getPath());
            checkUserFileDo.setFileSize(file.getSize());
            checkUserFileDo.setFileName(userFileById.getName());
        }
        return checkUserFileDo;
    }


    @Override
    public void renameUserFile(UserFile userFile) {
        if(userFile.getName() == null || "".equals(userFile.getName())){
            throw new NoNameException();
        }
        if (fileMapper.renameUserFileByIdAndUserId(userFile.getUserId(), userFile.getId(), userFile.getName()) <= 0) {
            throw new OperationFailedException();
        }
    }

    @Override
    public void moveUserFile(UserFile userFile) {
        LinkedList<UserFile> userFiles = fileMapper.selectByUserIdAndDirId(userFile.getUserId(), userFile.getParentId());
        for (UserFile file : userFiles) {
            if (file.getName().equals(userFile.getName())) {
                throw new DuplicateNameException();
            }
        }
        if (!this.isDir(userFile.getParentId())) {
            throw MoveUserFileException.NotDir();
        }
        if (fileMapper.moveUserFileByIdAndUserIdAndDirId(userFile.getUserId(), userFile.getId(), userFile.getParentId()) <= 0) {
            throw new OperationFailedException();
        }
    }

    @Override
    public void download(Long userFileId, HttpServletResponse response, HttpHeaders httpHeaders, String token) throws IOException {
        boolean rangeFlag = httpHeaders.getRange().size() == 0;
        User user = JWTUtil.getUserVale(token);
        int bufferSize = user.getRole().getDownloadLimit();
        long fileStartOff = 0L;
        long fileSize;
        //检测此用户是否有此文件
        CheckUserFileDo checkUserFileDo = this.getFilePath(userFileId, user.getId());

        String filePath;
        if (checkUserFileDo.getCheckStatus()) {
            filePath = checkUserFileDo.getFilePath();
            fileSize = checkUserFileDo.getFileSize();
            System.out.println(checkUserFileDo.getFileName());
            response.setHeader("Content-Disposition", "attachment;filename=" + checkUserFileDo.getFileName());
        } else {
            throw new FileNotFoundException();
        }
        Path targetFilePath = Paths.get(filePath);
        if (rangeFlag) {
            response.setStatus(HttpServletResponse.SC_OK);
            response.setHeader("Accept-Ranges", "bytes");
            response.setContentLengthLong(checkUserFileDo.getFileSize());
            FileUtil.downloadFile(targetFilePath, response.getOutputStream(), bufferSize, fileStartOff);
        } else {
            long offset = httpHeaders.getRange().get(0).getRangeStart(fileSize);
            long contentLength = fileSize - offset;
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            response.setContentLengthLong(contentLength);
            response.setHeader("Content-Range", "bytes " + offset + "-" + (fileSize - 1) + "/" + fileSize);
            FileUtil.downloadFile(targetFilePath, response.getOutputStream(), bufferSize, offset);
        }
    }

    @Override
    public List<FileVO> searchFile(Long userId, String filename) {
        Queue<FileVO> sortFileVoQueue = new LinkedList<>();
        FileVO fileVO;
        List<FileVO> fileVOList = new LinkedList<>();
        List<UserFile> fileList = fileMapper.selectByUserIdAndFilename(userId, filename);
        for (UserFile userFile : fileList) {
            fileVO = new FileVO();
            fileVO.setUserFile(userFile);
            //文件夹优先
            if (fileVO.getTypeId() == this.dirId) {
                fileVOList.add(fileVO);
            } else {
                sortFileVoQueue.add(fileVO);
            }
        }
        while (sortFileVoQueue.size() > 0) {
            fileVOList.add(sortFileVoQueue.poll());
        }
        return fileVOList;
    }

    @Override
    public void copy(Long userFileId, Long userId, Long target) {
        UserFile userFile = fileMapper.selectUserFileByIdAndUserId(userId, userFileId);
        if(userFile == null){
            throw new FileNotExistException();
        }
        if(!isDir(target)){
            throw new InvalidParamterException();
        }
        if(fileMapper.copyUserFileByFileIdAndUserId(userFileId,userId,target) <= 0){
            throw new OperationFailedException();
        }
    }

    @Override
    public Object uploadFileChunk(Long userId, MultipartFile uploadedChunk, String fileName, String fileMd5, Long fileSize, Integer chunkIndex, String chunkMd5, Long parentId, String token) {
        //用户空间不足，抛出异常
        if (isFull(fileSize, token)) {
            throw new NotEnoughSpaceException();
        }

        Path filePath = Paths.get(baseFilePath, fileMd5);

        //文件已存在，跳出方法
        if (filePath.toFile().exists() && filePath.toFile().isFile()) {
            return new ResponseWrapper(ResultCode.DATA_ALREADY_EXISTED);
        }

        //分块总数,分块数不会很大,直接强转
        int allChunkNum = (int) Math.ceil(fileSize / chunkSize);

        //检测文件夹是否存在 不存在创建
        FileUtil.createDirIfAbsent(filePath);

        Path chunkPath = Paths.get(filePath.toString(), chunkMd5);

        HashOperations<String, Object, Integer> hashOperations = redisTemplate.opsForHash();
        HashOperations<String, String, LinkedList<Integer>> failedHashOperation = redisTemplate.opsForHash();


        LinkedList<Integer> failedChunk = Optional.ofNullable(failedHashOperation.get("failedChunk", fileMd5))
                .orElse(new LinkedList<>());

        //上传块不是失败块的时候 将当前块index改为上传块index
        if (!failedChunk.contains(chunkIndex)) {
            hashOperations.put(redisCurrIndexKey, fileMd5, chunkIndex);
        }
        //先临时将失败块删除，防止两块相同的失败块被取出
        else {
            failedChunk.remove(chunkIndex);
            failedHashOperation.put(redisFailedChunkKey, fileMd5, failedChunk);
        }
        //文件上传
        try {
            //传输分块
            FileUtil.uploadFile((FileInputStream) uploadedChunk.getInputStream(), chunkPath);
            //保存分块信息
            FileUtil.saveChunkInfo(baseFilePath, chunkIndex, fileMd5, chunkMd5);
            if (chunkIndex == allChunkNum && failedChunk.size() == 0) {
                //分块校验,返回第一块失败块
                int failedChunkIndex = FileUtil.chunkChecker(fileMd5, baseFilePath, allChunkNum);
                if (failedChunkIndex == -1) {
                    FileUtil.mergeFiles(baseFilePath, fileMd5, allChunkNum);
                    Long fileId = insertFile(fileMd5, filePath.toString(), fileSize, getFileTypeId(fileName));
                    insertUserFile(fileName, userId, fileId, parentId);
                } else {
                    failedChunk.add(failedChunkIndex);
                    failedHashOperation.putIfAbsent(redisFailedChunkKey, fileMd5, failedChunk);
                }
            }
        } catch (IOException e) {
            logger.info("userId为:" + userId + "上传" + uploadedChunk.getOriginalFilename() + "失败");

            failedChunk.add(chunkIndex);
            failedHashOperation.putIfAbsent(redisFailedChunkKey, fileMd5, failedChunk);
            chunkPath.toFile().delete();
        }
        return new ResponseWrapper(ResultCode.SUCCESS);
    }


    @Override
    public Object flashDetection(Long userId, String fileMd5, Long fileParentId, String fileName, String token) {
        File file = fileMapper.selectFileByMd5(fileMd5);
        if (checkFileExist(fileMd5)) {
            if (this.isFull(file.getSize(), token)) {
                throw new NotEnoughSpaceException();
            }
            UserFile userFile = new UserFile();
            userFile.setUserId(userId);
            userFile.setFileId(file.getId());
            userFile.setParentId(fileParentId);
            userFile.setName(fileName);
            Integer insertUserFileResult = fileMapper.insertUserFile(userFile);
            return insertUserFileResult > 0 ? new ResponseWrapper(ResultCode.SUCCESS) : new ResponseWrapper(ResultCode.USER_FILE_INSERT_ERROR);
        }
        return new ResponseWrapper(ResultCode.SECOND_PASS_FAILED);
    }

    private boolean checkFileExist(String fileMd5) {
        return fileMapper.selectFileByMd5(fileMd5) != null;
    }

    private boolean isDir(Long fileId) {
        return fileMapper.selectDirById(fileId).size() > 0;
    }

    private boolean isFull(Long newFileSize, String token) {
        Long diskSize = JWTUtil.getValue(token, "diskSize", Long.class);
        Long userId = JWTUtil.getValue(token, "id", Long.class);
        Long userFileUsage = fileMapper.selectUserFileUsage(userId);
        return Optional.ofNullable(userFileUsage).orElse(0L) + newFileSize > diskSize;
    }

    private Integer getFileTypeId(String fileName) {
        String[] stringArr = fileName.split("\\.");
        Integer misTypeId = 6;
        String suffix = stringArr[stringArr.length - 1];
        FileType type = typeMapper.findTypeBySuffix(suffix);
        return Optional.ofNullable(type).map(FileType::getId).orElse(misTypeId);
    }

    private void insertUserFile(String fileName, Long userId, Long fileId, Long parentId) {
        UserFile userFile = new UserFile();
        userFile.setName(fileName);
        userFile.setUserId(userId);
        userFile.setFileId(fileId);
        userFile.setParentId(parentId);
        fileMapper.insertUserFile(userFile);
    }

    private Long insertFile(String fileMd5, String filePath, Long fileSize, Integer fileTypeId) {
        File file = new File();
        file.setMd5(fileMd5);
        file.setPath(filePath);
        file.setSize(fileSize);
        file.setTypeId(fileTypeId);
        if (fileMapper.insertFile(file) <= 0) {
            throw new OperationFailedException();
        }
        return file.getId();
    }
}
